package simplifiedPastry;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Random;

import org.junit.Test;

/**
 * Class representing NodeId which is 128 bit unsigned value.
 * 
 * @author Stanisław Ogórkis
 */
public class NodeId implements Comparable<NodeId>, Serializable {
	private static final long serialVersionUID = -1344941108589711577L;
	
	private byte[] byteId;
	private BigInteger bigIntegerId;

	private final static byte[] nullId = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
	private static Random rand = new Random();
	private static BigInteger maxId;
	static {
		BigInteger i = new BigInteger("2");
		i = i.pow(128);
		i = i.subtract(new BigInteger("1"));
		maxId = i;
	}
	
	public static byte[] encodeId(String nodeIdS) {
		byte id[] = new byte[16];
		BigInteger num = new BigInteger(nodeIdS, 10);
		byte[] numbytes = num.toByteArray();
		
		if(num.compareTo(new BigInteger("0")) < 0) {
			
			int stop = 16-numbytes.length;
			for(int i = 0; i < stop; ++i) {
				id[i] = (byte) 0xFF;
			}
		}
		
		int start = 16-numbytes.length;
		for(int i = start; i < 16; ++i) {
			id[i] = numbytes[i-start];
		}
		
		num = new BigInteger(id);
		
		return id;
	}
	
	public static String decodeId(byte[] id) {
		if(id.length != 16) {
			return "";
		}
		
		BigInteger num = new BigInteger(id);
		
		return num.toString();
	}
	
	public int sameGroupNum(NodeId otherId) {
		int i;
		for(i = 0; i < 32; ++i) {
			if(i % 2 == 0) {
				if ((byteId[i/2] & 15) != (otherId.byteId[i/2] & 15))
					break;
			} else {
				if ((byteId[i/2] & -16) != (otherId.byteId[i/2] & -16))
					break;
			}
		}
		if (i < 0 || i > 31) {
			System.err.println("!!!!!!!!!!!row " + i);
		}
		return (i == 0) ? 0 : i-1;
	}
	
	/**
	 * Constructs new NodeId based on array of bytes in BigEndian order.
	 * If array length is not 16, Null NodeId (all bits 0) is constructed.
	 * @param val
	 */
	public NodeId(byte[] val) {
		if (val.length != 16) {
			byteId = nullId;
		} else {
			byteId = Arrays.copyOf(val, 16);
			bigIntegerId = new BigInteger(1,byteId);
		}		
	}
	
	/**
	 * Constructs new NodeId based on bytes in BigEndian order from buff, moved
	 * right at offset. If buff.length - offset < 16, Null NodeId (all bits 0)
	 * is constructed.
	 * @param buff
	 * @param offset
	 */
	public NodeId(byte[] buff, int offset) {
		if (buff.length - offset < 16) {
			byteId = nullId;
		} else {
			byteId = Arrays.copyOfRange(buff, offset, offset + 16);
			bigIntegerId = new BigInteger(1,byteId);
		}		
	}
	
	/**
	 * If all bits of NodeId equals 0 returns true.
	 * @return true if all bits of nodeId equals 0.
	 */
	public boolean isNull() {
		return Arrays.equals(byteId, nullId);
	}
	
	/**
	 * Returns row in routingTable in which otherId should be put.
	 * @param otherId the NodeId to be compared.
	 * @return row index in routing table.
	 */
	public int routingTableRow(NodeId otherId) {
		int i;
		for(i = 0; i < 32; ++i) {
			if(i % 2 == 0) {
				if ((byteId[i/2] & 15) != (otherId.byteId[i/2] & 15))
					break;
			} else {
				if ((byteId[i/2] & -16) != (otherId.byteId[i/2] & -16))
					break;
			}
		}
		if (i < 0 || i > 31) {
			System.err.println("!!!!!!!!!!!row " + i);
		}
		return i;
	}
	

	/**
	 * Returns column in routingTable in which otherId should be put.
	 * @param otherId the NodeId to be compared.
	 * @return
	 */
	public int routingTableColumn(NodeId otherId) {
		return routingTableColumn(otherId, routingTableRow(otherId));
	}
	
	/**
	 * Returns column in routingTable in which otherId should be put.
	 * @param otherId the NodeId to be compared.
	 * @param rowHint row index in routing table.
	 * @return
	 */
	public int routingTableColumn(NodeId otherId, int rowHint) {
		int num = 0;
		if (rowHint % 2 == 0) {
			num = otherId.byteId[(rowHint/2)] & 15;
			if (num < 0 || num > 15) {
				System.err.println("!!!!!!!!!!!column1 " + num);
			}
		} else {
			num = ((otherId.byteId[(rowHint/2)] & -16) >>> 4) & 15;
		if (num < 0 || num > 15) {
			System.err.println("!!!!!!!!!!!column2 " + num);
		}
		}
		if (num < 0 || num > 15) {
			System.err.println("!!!!!!!!!!!column " + num);
		}
		return num;
	}
	
	/**
	 * Calculate distance on circular field from 0 to 2^128-1.
	 * @param otherId nodeId of to be calculated.
	 * @return
	 */
	public BigInteger distance(NodeId otherId) {
		BigInteger smaller, bigger;
		if (bigIntegerId.compareTo(otherId.bigIntegerId) > 0) {
			smaller = otherId.bigIntegerId;
			bigger	= bigIntegerId;
		} else {
			smaller = bigIntegerId;
			bigger	= otherId.bigIntegerId;
		}
		BigInteger d1 = smaller.add(maxId.subtract(bigger));
		BigInteger d2 = bigger.subtract(smaller);
		if (d1.signum() == -1) {
			System.err.println("Negative distance!" + d1 + ":" + d2 + ";" + maxId);
			System.err.println("bigger:" + bigger + " smaller:" + smaller);
		}
		return (d1.compareTo(d2) < 0) ? d1 : d2;
	}
	
	/**
	 * Returns random generated NodeId.
	 * @return random generated NodeId.
	 */
	public static NodeId random() {
		NodeId returnId = new NodeId();
		rand.nextBytes(returnId.byteId);
		returnId.bigIntegerId = new BigInteger(1, returnId.byteId);
		return returnId;
	}
	
	/**
	 * Returns NodeId byte representation in BigEndian order.
	 * @return NodeId byte representation in BigEndian order.
	 */
	public byte[] getByteNodeId() {
		return byteId;		
	}
	
	public String binaryString() {
		return bigIntegerId.toString(2);
	}
	
	@Override
	public String toString() {
		return bigIntegerId.toString(16);
	}
	
	@Override
	public int hashCode() {
		return Arrays.hashCode(byteId);
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof NodeId) {
			NodeId nodeId = (NodeId) obj;
			return compareTo(nodeId) == 0;
		}
		return false;
	}

	@Override
	public int compareTo(NodeId o) {
		return bigIntegerId.compareTo(o.bigIntegerId);
	}
	
	private NodeId() {
		byteId = new byte[16];
	}
	
	public static class Tester {

		@Test
		public void testNodeIdByteArray() {
			byte[] array1 = new byte[20];
			byte[] array2 = new byte[10];
			byte[] array3 = new byte[16];
			array3[10] = 8;
			NodeId nodeId1 = new NodeId(array1);
			NodeId nodeId2 = new NodeId(array2);
			NodeId nodeId3 = new NodeId(array3);
			assertTrue("NodeId " + array1 + " is not null.", nodeId1.isNull());
			assertTrue("NodeId " + array2 + " is not null.", nodeId2.isNull());
			assertFalse("NodeId " + array3 + " is null.", nodeId3.isNull());
		}

		@Test
		public void testToString() {
			byte[] array1 = new byte[16];
			array1[15] = 1;
			NodeId node1 = new NodeId(array1);
			assertFalse(node1.isNull());
			assertTrue(node1.toString().equals("1"));
		}

		@Test
		public void testEqualsObject() {
			byte[] array1 = new byte[16];
			array1[15] = 1;
			byte[] array2 = new byte[16];
			array2[15] = 7;
			byte[] array3 = new byte[16];
			array3[15] = 7;
			NodeId nodeId1 = new NodeId(array1);
			NodeId nodeId2 = new NodeId(array2);
			NodeId nodeId3 = new NodeId(array3);
			assertFalse(nodeId1.equals(nodeId2));
			assertTrue(nodeId2.equals(nodeId3));
		}

		@Test
		public void testCompareTo() {
			byte[] array1 = new byte[16];
			array1[15] = 1;
			byte[] array2 = new byte[16];
			array2[15] = 7;
			byte[] array3 = new byte[16];
			array3[15] = 20;
			NodeId nodeId1 = new NodeId(array1);
			NodeId nodeId2 = new NodeId(array2);
			NodeId nodeId3 = new NodeId(array3);
			assertTrue(nodeId1.compareTo(nodeId2) == -1);
			assertTrue(nodeId1.compareTo(nodeId3) == -1);
			assertTrue(nodeId1.compareTo(nodeId1) == 0);
			assertTrue(nodeId2.compareTo(nodeId3) == -1);
			assertTrue(nodeId3.compareTo(nodeId2) == 1);
			assertTrue(nodeId3.compareTo(nodeId1) == 1);
			assertTrue(nodeId3.compareTo(nodeId3) == 0);
		}
		
		@Test
		public void testRoutingTable() {
			byte[] array1 = new byte[16];
			array1[15] = 1;
			byte[] array2 = new byte[16];
			array2[15] = 31;
			byte[] array3 = new byte[16];
			array3[15] = 63;
			byte[] array4 = new byte[16];
			array4[15] = 63;
			array4[0] = 64;
			NodeId nodeId1 = new NodeId(array1);
			NodeId nodeId2 = new NodeId(array2);
			NodeId nodeId3 = new NodeId(array3);
			NodeId nodeId4 = new NodeId(array4);
			assertTrue(nodeId1.routingTableRow(nodeId2) == 0);
			assertTrue(nodeId2.routingTableRow(nodeId3) == 1);
			assertTrue(nodeId4.routingTableRow(nodeId3) == 31);
			assertTrue(nodeId4.routingTableRow(nodeId4) == 32);
			assertTrue(nodeId1.routingTableColumn(nodeId1, 0) == 1);
			assertTrue(nodeId1.routingTableColumn(nodeId1, 10) == 0);
			assertTrue(nodeId1.routingTableColumn(nodeId2, 1) == 1);
			assertTrue(nodeId1.routingTableColumn(nodeId2, 0) == 15);
			assertTrue(nodeId1.routingTableColumn(nodeId3, 0) == 15);
			assertTrue(nodeId1.routingTableColumn(nodeId3, 1) == 3);
			assertTrue(nodeId1.routingTableColumn(nodeId4, 30) == 0);
			assertTrue(nodeId1.routingTableColumn(nodeId4, 31) == 4);
		}

		@Test
		public void testDistance() {
			NodeId nodeId1 = new NodeId(NodeId.encodeId("322144675975321302232694402351234612379"));
			NodeId nodeId2 = new NodeId(NodeId.encodeId("302474799546980725640807455753223873474"));
			System.out.println(nodeId1.distance(nodeId2));
		}
	}
}
